package org.hxzon.util.json;

//Copyright 2007-2013 The Apache Software Foundation
//
//Licensed under the Apache License, Version 2.0 (the "License");
//you may not use this file except in compliance with the License.
//You may obtain a copy of the License at
//
//http://www.apache.org/licenses/LICENSE-2.0
//
//Unless required by applicable law or agreed to in writing, software
//distributed under the License is distributed on an "AS IS" BASIS,
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//See the License for the specific language governing permissions and
//limitations under the License.

//package org.apache.tapestry5.json;

/*
* Copyright (c) 2002 JSON.org
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* The Software shall be used for Good, not Evil.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* A JSONObject is an unordered collection of name/value pairs. Its external form is a string wrapped in curly braces
* with colons between the names and values, and commas between the values and names. The internal form is an object
* having <code>get</code> and <code>opt</code> methods for accessing the values by name, and <code>put</code> methods
* for adding or replacing values by name. The values can be any of these types: <code>Boolean</code>,
* {@link org.apache.tapestry5.json.JSONArray}, {@link org.apache.tapestry5.json.JSONLiteral}, <code>JSONObject</code>,
* <code>Number</code>, <code>String</code>, or the <code>JSONObject.NULL</code> object. A JSONObject constructor can be
* used to convert an external form JSON text into
* an internal form whose values can be retrieved with the <code>get</code> and <code>opt</code> methods, or to convert
* values into a JSON text using the <code>put</code> and <code>toString</code> methods. A <code>get</code> method
* returns a value if one can be found, and throws an exception if one cannot be found. An <code>opt</code> method
* returns a default value instead of throwing an exception, and so is useful for obtaining optional values.
* <p/>
* The generic <code>get()</code> and <code>opt()</code> methods return an object, which you can cast or query for type.
* There are also typed <code>get</code> and <code>opt</code> methods that do type checking and type coersion for you.
* <p/>
* The <code>put</code> methods adds values to an object. For example,
* <p/>
* <pre>
* myString = new JSONObject().put(&quot;JSON&quot;, &quot;Hello, World!&quot;).toString();
* </pre>
* <p/>
* produces the string <code>{"JSON": "Hello, World"}</code>.
* <p/>
* The texts produced by the <code>toString</code> methods strictly conform to the JSON syntax rules. The constructors
* are more forgiving in the texts they will accept:
* <ul>
* <li>An extra <code>,</code>&nbsp;<small>(comma)</small> may appear just before the closing brace.</li>
* <li>Strings may be quoted with <code>'</code>&nbsp;<small>(single quote)</small>.</li>
* <li>Strings do not need to be quoted at all if they do not begin with a quote or single quote, and if they do not
* contain leading or trailing spaces, and if they do not contain any of these characters: <code>{ }
* [ ] / \ : , = ; #</code> and if they do not look like numbers and if they are not the reserved words
* <code>true</code>, <code>false</code>, or <code>null</code>.</li>
* <li>Keys can be followed by <code>=</code> or <code>=></code> as well as by <code>:</code>.</li>
* <li>Values can be followed by <code>;</code> <small>(semicolon)</small> as well as by <code>,</code>
* <small>(comma)</small>.</li>
* <li>Numbers may have the <code>0-</code> <small>(octal)</small> or <code>0x-</code> <small>(hex)</small> prefix.</li>
* <li>Comments written in the slashshlash, slashstar, and hash conventions will be ignored.</li>
* </ul>
* <hr/>
* <p/>
* This class, and the other related classes, have been heavily modified from the original source, to fit Tapestry
* standards and to make use of JDK 1.5 features such as generics. Further, since the interest of Tapestry is primarily
* constructing JSON (and not parsing it), many of the non-essential methods have been removed (since the original code
* came with no tests).
* <p/>
* Finally, support for the {@link org.apache.tapestry5.json.JSONLiteral} type has been added, which allows the exact
* output to be controlled; useful when a JSONObject is being used as a configuration object, and must contain values
* that are not simple data, such as an inline function (making the result not JSON).
*
* @author JSON.org
* @version 2
*/
@SuppressWarnings({ "serial" })
public final class JSONObject extends JSONCollection {

    /**
     * JSONObject.NULL is equivalent to the value that JavaScript calls null, whilst Java's null is equivalent to the
     * value that JavaScript calls undefined.
     */
    private static final class Null implements JSONString, Serializable {
        /**
         * A Null object is equal to the null value and to itself.
         *
         * @param object
         *         An object to test for nullness.
         * @return true if the object parameter is the JSONObject.NULL object or null.
         */
        @Override
        public boolean equals(Object object) {
            return object == null || object == this;
        }

        /**
         * Get the "null" string value.
         *
         * @return The string "null".
         */
        @Override
        public String toString() {
            return "null";
        }

        @Override
        public String toJSONString() {
            return "null";
        }

        // Serialization magic: after de-serializing, it will be back to the singleton instance of NULL.
        private Object readResolve() throws ObjectStreamException {
            return NULL;
        }
    }

    /**
     * The map where the JSONObject's properties are kept.
     */
    private final Map<String, Object> properties = new LinkedHashMap<String, Object>();

    /**
     * It is sometimes more convenient and less ambiguous to have a <code>NULL</code> object than to use Java's
     * <code>null</code> value. <code>JSONObject.NULL.equals(null)</code> returns <code>true</code>.
     * <code>JSONObject.NULL.toString()</code> returns <code>"null"</code>.
     */
    public static final Object NULL = new Null();

    /**
     * Construct an empty JSONObject.
     */
    public JSONObject() {
    }

    /**
     * Returns a new JSONObject that is a shallow copy of this JSONObject.
     *
     * @since 5.4
     */
    public JSONObject copy() {
        JSONObject dupe = new JSONObject();
        dupe.properties.putAll(properties);

        return dupe;
    }

    /**
     * Constructs a new JSONObject using a series of String keys and object values.
     * Object values sholuld be compatible with {@link #put(String, Object)}. Keys must be strings
     * (toString() will be invoked on each key).
     * <p/>
     * Prior to release 5.4, keysAndValues was type String...; changing it to Object... makes
     * it much easier to initialize a JSONObject in a single statement, which is more readable.
     *
     * @since 5.2.0
     */
    public JSONObject(Object... keysAndValues) {
        int i = 0;

        while (i < keysAndValues.length) {
            put(keysAndValues[i++].toString(), keysAndValues[i++]);
        }
    }

    /**
     * Construct a JSONObject from a subset of another JSONObject. An array of strings is used to identify the keys that
     * should be copied. Missing keys are ignored.
     *
     * @param source
     *         A JSONObject.
     * @param propertyNames
     *         The strings to copy.
     * @throws RuntimeException
     *         If a value is a non-finite number.
     */
    public JSONObject(JSONObject source, String... propertyNames) {
        for (String name : propertyNames) {
            Object value = source.opt(name);

            if (value != null)
                put(name, value);
        }
    }

    /**
     * Construct a JSONObject from a JSONTokener.
     *
     * @param x
     *         A JSONTokener object containing the source string. @ If there is a syntax error in the source string.
     */
    JSONObject(JSONTokener x) {
        String key;

        if (x.nextClean() != '{') {
            throw x.syntaxError("A JSONObject text must begin with '{'");
        }

        while (true) {
            char c = x.nextClean();
            switch (c) {
            case 0:
                throw x.syntaxError("A JSONObject text must end with '}'");
            case '}':
                return;
            default:
                x.back();
                key = x.nextValue().toString();
            }

            /*
             * The key is followed by ':'. We will also tolerate '=' or '=>'.
             */

            c = x.nextClean();
            if (c == '=') {
                if (x.next() != '>') {
                    x.back();
                }
            } else if (c != ':') {
                throw x.syntaxError("Expected a ':' after a key");
            }
            put(key, x.nextValue());

            /*
             * Pairs are separated by ','. We will also tolerate ';'.
             */

            switch (x.nextClean()) {
            case ';':
            case ',':
                if (x.nextClean() == '}') {
                    return;
                }
                x.back();
                break;
            case '}':
                return;
            default:
                throw x.syntaxError("Expected a ',' or '}'");
            }
        }
    }

    /**
     * Construct a JSONObject from a string. This is the most commonly used JSONObject constructor.
     *
     * @param string
     *         A string beginning with <code>{</code>&nbsp;<small>(left brace)</small> and ending with <code>}</code>
     *         &nbsp;<small>(right brace)</small>.
     * @throws RuntimeException
     *         If there is a syntax error in the source string.
     */
    public JSONObject(String string) {
        this(new JSONTokener(string));
    }

    /**
     * Accumulate values under a key. It is similar to the put method except that if there is already an object stored
     * under the key then a JSONArray is stored under the key to hold all of the accumulated values. If there is already
     * a JSONArray, then the new value is appended to it. In contrast, the put method replaces the previous value.
     *
     * @param key
     *         A key string.
     * @param value
     *         An object to be accumulated under the key.
     * @return this.
     * @throws {@link
     *         RuntimeException} If the value is an invalid number or if the key is null.
     */
    public JSONObject accumulate(String key, Object value) {
        testValidity(value);

        Object existing = opt(key);

        if (existing == null) {
            // Note that the original implementation of this method contradicted the method
            // documentation.
            put(key, value);
            return this;
        }

        if (existing instanceof JSONArray) {
            ((JSONArray) existing).put(value);
            return this;
        }

        // Replace the existing value, of any type, with an array that includes both the
        // existing and the new value.

        put(key, new JSONArray().put(existing).put(value));

        return this;
    }

    /**
     * Append values to the array under a key. If the key does not exist in the JSONObject, then the key is put in the
     * JSONObject with its value being a JSONArray containing the value parameter. If the key was already associated
     * with a JSONArray, then the value parameter is appended to it.
     *
     * @param key
     *         A key string.
     * @param value
     *         An object to be accumulated under the key.
     * @return this. @ If the key is null or if the current value associated with the key is not a JSONArray.
     */
    public JSONObject append(String key, Object value) {
        testValidity(value);
        Object o = opt(key);
        if (o == null) {
            put(key, new JSONArray().put(value));
        } else if (o instanceof JSONArray) {
            put(key, ((JSONArray) o).put(value));
        } else {
            throw new RuntimeException("JSONObject[" + quote(key) + "] is not a JSONArray.");
        }

        return this;
    }

    /**
     * Produce a string from a double. The string "null" will be returned if the number is not finite.
     *
     * @param d
     *         A double.
     * @return A String.
     */
    static String doubleToString(double d) {
        if (Double.isInfinite(d) || Double.isNaN(d)) {
            return "null";
        }

        // Shave off trailing zeros and decimal point, if possible.

        String s = Double.toString(d);
        if (s.indexOf('.') > 0 && s.indexOf('e') < 0 && s.indexOf('E') < 0) {
            while (s.endsWith("0")) {
                s = s.substring(0, s.length() - 1);
            }
            if (s.endsWith(".")) {
                s = s.substring(0, s.length() - 1);
            }
        }
        return s;
    }

    /**
     * Get the value object associated with a key.
     *
     * @param key
     *         A key string.
     * @return The object associated with the key.
     * @throws RuntimeException
     *         if the key is not found.
     * @see #opt(String)
     */
    public Object get(String key) {
        Object o = opt(key);
        if (o == null) {
            throw new RuntimeException("JSONObject[" + quote(key) + "] not found.");
        }

        return o;
    }

    /**
     * Get the boolean value associated with a key.
     *
     * @param key
     *         A key string.
     * @return The truth.
     * @throws RuntimeException
     *         if the value does not exist, is not a Boolean or the String "true" or "false".
     */
    public boolean getBoolean(String key) {
        Object o = get(key);

        if (o instanceof Boolean)
            return o.equals(Boolean.TRUE);

        if (o instanceof String) {
            String value = (String) o;

            if (value.equalsIgnoreCase("true"))
                return true;

            if (value.equalsIgnoreCase("false"))
                return false;
        }

        throw new RuntimeException("JSONObject[" + quote(key) + "] is not a Boolean.");
    }

    /**
     * Get the double value associated with a key.
     *
     * @param key
     *         A key string.
     * @return The numeric value. @throws RuntimeException if the key is not found or if the value is not a Number object and cannot be
     *         converted to a number.
     */
    public double getDouble(String key) {
        Object value = get(key);

        try {
            if (value instanceof Number)
                return ((Number) value).doubleValue();

            // This is a bit sloppy for the case where value is not a string.

            return Double.valueOf((String) value);
        } catch (Exception e) {
            throw new RuntimeException("JSONObject[" + quote(key) + "] is not a number.");
        }
    }

    /**
     * Get the int value associated with a key. If the number value is too large for an int, it will be clipped.
     *
     * @param key
     *         A key string.
     * @return The integer value.
     * @throws RuntimeException
     *         if the key is not found or if the value cannot be converted to an integer.
     */
    public int getInt(String key) {
        Object value = get(key);

        if (value instanceof Number)
            return ((Number) value).intValue();

        // Very inefficient way to do this!
        return (int) getDouble(key);
    }

    /**
     * Get the JSONArray value associated with a key.
     *
     * @param key
     *         A key string.
     * @return A JSONArray which is the value.
     * @throws RuntimeException
     *         if the key is not found or if the value is not a JSONArray.
     */
    public JSONArray getJSONArray(String key) {
        Object o = get(key);
        if (o instanceof JSONArray) {
            return (JSONArray) o;
        }

        throw new RuntimeException("JSONObject[" + quote(key) + "] is not a JSONArray.");
    }

    /**
     * Get the JSONObject value associated with a key.
     *
     * @param key
     *         A key string.
     * @return A JSONObject which is the value.
     * @throws RuntimeException
     *         if the key is not found or if the value is not a JSONObject.
     */
    public JSONObject getJSONObject(String key) {
        Object o = get(key);
        if (o instanceof JSONObject) {
            return (JSONObject) o;
        }

        throw new RuntimeException("JSONObject[" + quote(key) + "] is not a JSONObject.");
    }

    /**
     * Get the long value associated with a key. If the number value is too long for a long, it will be clipped.
     *
     * @param key
     *         A key string.
     * @return The long value.
     * @throws RuntimeException
     *         if the key is not found or if the value cannot be converted to a long.
     */
    public long getLong(String key) {
        Object o = get(key);
        return o instanceof Number ? ((Number) o).longValue() : (long) getDouble(key);
    }

    /**
     * Get the string associated with a key.
     *
     * @param key
     *         A key string.
     * @return A string which is the value.
     * @throws RuntimeException
     *         if the key is not found.
     */
    public String getString(String key) {
        return get(key).toString();
    }

    /**
     * Determine if the JSONObject contains a specific key.
     *
     * @param key
     *         A key string.
     * @return true if the key exists in the JSONObject.
     */
    public boolean has(String key) {
        return properties.containsKey(key);
    }

    /**
     * Determine if the value associated with the key is null or if there is no value.
     *
     * @param key
     *         A key string.
     * @return true if there is no value associated with the key or if the value is the JSONObject.NULL object.
     */
    public boolean isNull(String key) {
        return JSONObject.NULL.equals(opt(key));
    }

    /**
     * Get an enumeration of the keys of the JSONObject. Caution: the set should not be modified.
     *
     * @return An iterator of the keys.
     */
    public Set<String> keys() {
        return properties.keySet();
    }

    /**
     * Get the number of keys stored in the JSONObject.
     *
     * @return The number of keys in the JSONObject.
     */
    public int length() {
        return properties.size();
    }

    /**
     * Produce a JSONArray containing the names of the elements of this JSONObject.
     *
     * @return A JSONArray containing the key strings, or null if the JSONObject is empty.
     */
    public JSONArray names() {
        JSONArray ja = new JSONArray();

        for (String key : keys()) {
            ja.put(key);
        }

        return ja.length() == 0 ? null : ja;
    }

    /**
     * Produce a string from a Number.
     *
     * @param n
     *         A Number
     * @return A String. @ If n is a non-finite number.
     */
    static String numberToString(Number n) {
        assert n != null;

        testValidity(n);

        // Shave off trailing zeros and decimal point, if possible.

        String s = n.toString();
        if (s.indexOf('.') > 0 && s.indexOf('e') < 0 && s.indexOf('E') < 0) {
            while (s.endsWith("0")) {
                s = s.substring(0, s.length() - 1);
            }
            if (s.endsWith(".")) {
                s = s.substring(0, s.length() - 1);
            }
        }
        return s;
    }

    /**
     * Get an optional value associated with a key.
     *
     * @param key
     *         A key string.
     * @return An object which is the value, or null if there is no value.
     * @see #get(String)
     */
    public Object opt(String key) {
        return properties.get(key);
    }

    /**
     * Put a key/value pair in the JSONObject. If the value is null, then the key will be removed from the JSONObject if
     * it is present.
     *
     * @param key
     *         A key string.
     * @param value
     *         An object which is the value. It should be of one of these types: Boolean, Double, Integer,
     *         JSONArray, JSONObject, JSONLiteral, Long, String, or the JSONObject.NULL object.
     * @return this.
     * @throws RuntimeException
     *         If the value is non-finite number or if the key is null.
     */
    public JSONObject put(String key, Object value) {
        assert key != null;

        if (value != null) {
            testValidity(value);
            properties.put(key, value);
        } else {
            remove(key);
        }

        return this;
    }

    /**
     * Produce a string in double quotes with backslash sequences in all the right places. A backslash will be inserted
     * within </, allowing JSON text to be delivered in HTML. In JSON text, a string cannot contain a control character
     * or an unescaped quote or backslash.
     *
     * @param string
     *         A String
     * @return A String correctly formatted for insertion in a JSON text.
     */
    public static String quote(String string) {
        if (string == null || string.length() == 0) {
            return "\"\"";
        }

        char b;
        char c = 0;
        int i;
        int len = string.length();
        StringBuilder buffer = new StringBuilder(len + 4);
        String t;

        buffer.append('"');
        for (i = 0; i < len; i += 1) {
            b = c;
            c = string.charAt(i);
            switch (c) {
            case '\\':
            case '"':
                buffer.append('\\');
                buffer.append(c);
                break;
            case '/':
                if (b == '<') {
                    buffer.append('\\');
                }
                buffer.append(c);
                break;
            case '\b':
                buffer.append("\\b");
                break;
            case '\t':
                buffer.append("\\t");
                break;
            case '\n':
                buffer.append("\\n");
                break;
            case '\f':
                buffer.append("\\f");
                break;
            case '\r':
                buffer.append("\\r");
                break;
            default:
                if (c < ' ' || (c >= '\u0080' && c < '\u00a0') || (c >= '\u2000' && c < '\u2100')) {
                    t = "000" + Integer.toHexString(c);
                    buffer.append("\\u").append(t.substring(t.length() - 4));
                } else {
                    buffer.append(c);
                }
            }
        }
        buffer.append('"');
        return buffer.toString();
    }

    /**
     * Remove a name and its value, if present.
     *
     * @param key
     *         The name to be removed.
     * @return The value that was associated with the name, or null if there was no value.
     */
    public Object remove(String key) {
        return properties.remove(key);
    }

    @SuppressWarnings("rawtypes")
    private static final Class[] ALLOWED = new Class[] { String.class, Boolean.class, Number.class, JSONObject.class, JSONArray.class, JSONString.class, JSONLiteral.class, Null.class };

    /**
     * Throw an exception if the object is an NaN or infinite number, or not a type which may be stored.
     *
     * @param value
     *         The object to test. @ If o is a non-finite number.
     */
    static void testValidity(Object value) {
        if (value == null)
            return;

        boolean found = false;
        Class<?> actual = value.getClass();

        for (Class<?> allowed : ALLOWED) {
            if (allowed.isAssignableFrom(actual)) {
                found = true;
                break;
            }
        }

        if (!found) {
            List<String> typeNames = new ArrayList<String>();

            for (Class<?> c : ALLOWED) {
                String name = c.getName();

                if (name.startsWith("java.lang."))
                    name = name.substring(10);

                typeNames.add(name);
            }

            Collections.sort(typeNames);

            StringBuilder joined = new StringBuilder();
            String sep = "";

            for (String name : typeNames) {
                joined.append(sep);
                joined.append(name);

                sep = ", ";
            }

            String message = String.format("JSONObject properties may be one of %s. Type %s is not allowed.", joined.toString(), actual.getName());

            throw new RuntimeException(message);
        }

        if (value instanceof Double) {
            Double asDouble = (Double) value;

            if (asDouble.isInfinite() || asDouble.isNaN()) {
                throw new RuntimeException("JSON does not allow non-finite numbers.");
            }

            return;
        }

        if (value instanceof Float) {
            Float asFloat = (Float) value;

            if (asFloat.isInfinite() || asFloat.isNaN()) {
                throw new RuntimeException("JSON does not allow non-finite numbers.");
            }

        }

    }

    /**
     * Prints the JSONObject using the session.
     *
     * @since 5.2.0
     */
    @Override
    void print(JSONPrintSession session) {
        session.printSymbol('{');

        session.indent();

        boolean comma = false;

        for (String key : keys()) {
            if (comma)
                session.printSymbol(',');

            session.newline();

            session.printQuoted(key);

            session.printSymbol(':');

            printValue(session, properties.get(key));

            comma = true;
        }

        session.outdent();

        if (comma)
            session.newline();

        session.printSymbol('}');
    }

    /**
     * Prints a value (a JSONArray or JSONObject, or a value stored in an array or object) using
     * the session.
     *
     * @since 5.2.0
     */
    static void printValue(JSONPrintSession session, Object value) {
        if (value instanceof JSONObject) {
            ((JSONObject) value).print(session);
            return;
        }

        if (value instanceof JSONArray) {
            ((JSONArray) value).print(session);
            return;
        }

        if (value instanceof JSONString) {
            String printValue = ((JSONString) value).toJSONString();

            session.print(printValue);

            return;
        }

        if (value instanceof Number) {
            String printValue = numberToString((Number) value);
            session.print(printValue);
            return;
        }

        if (value instanceof Boolean) {
            session.print(value.toString());

            return;
        }

        // Otherwise it really should just be a string. Nothing else can go in.
        session.printQuoted(value.toString());
    }

    /**
     * Returns true if the other object is a JSONObject and its set of properties matches this object's properties.
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == null)
            return false;

        if (!(obj instanceof JSONObject))
            return false;

        JSONObject other = (JSONObject) obj;

        return properties.equals(other.properties);
    }

    /**
     * Returns a Map of the keys and values of the JSONObject. The returned map is unmodifiable.
     * Note that changes to the JSONObject will be reflected in the map. In addition, null values in the JSONObject
     * are represented as {@link JSONObject#NULL} in the map.
     *
     * @return unmodifiable map of properties and values
     * @since 5.4
     */
    public Map<String, Object> toMap() {
        return Collections.unmodifiableMap(properties);
    }

    /**
     * Invokes {@link #put(String, Object)} for each value from the map.
     *
     * @param newProperties
     *         to add to this JSONObject
     * @return this JSONObject
     * @since 5.4
     */
    public JSONObject putAll(Map<String, ?> newProperties) {
        assert newProperties != null;

        for (Map.Entry<String, ?> e : newProperties.entrySet()) {
            put(e.getKey(), e.getValue());
        }

        return this;
    }

    /**
     * Navigates into a nested JSONObject, creating the JSONObject if necessary. They key must not exist,
     * or must be a JSONObject.
     *
     * @param key
     * @return the nested JSONObject
     * @throws IllegalStateException
     *         if the current value for the key is not null and not JSONObject
     */
    public JSONObject in(String key) {
        assert key != null;

        Object nested = properties.get(key);

        if (nested != null && !(nested instanceof JSONObject)) {
            throw new IllegalStateException(String.format("JSONObject[%s] is not a JSONObject.", quote(key)));
        }

        if (nested == null) {
            nested = new JSONObject();
            properties.put(key, nested);
        }

        return (JSONObject) nested;
    }
}
